"""Functions for various cave-like maps"""

from __future__ import division

from pyre.core import types, dice

def fill_rock(world, rock=types.ColoredChar('#')):
    """
    fill_rock(World): None
    Fills a map complete with rock
    """

    for x in range(world.cols):
        for y in range(world.rows):
            world.set_tile((x, y), rock, False)


def cavern(world, wall=types.ColoredChar('#'), floor=types.ColoredChar('.')):
    """
    cavern(World, ColoredChar, ColoredChar): None
    Generates a cave map on the World using cellular automata
    """

    fill_rock(world)
    automata = _Automata(world, wall, floor)
    connected_cave = False
    while not connected_cave:
        automata.restart()
        automata.apply_rule()
        connected_cave = automata.try_connect()


class _Automata(object):

    def __init__(self, world, wall, floor):
        self.world = world
        self.cols = world.cols
        self.rows = world.rows
        self.wall = wall
        self.floor = floor
        self.buffer = [[False for _ in range(self.rows)]
                       for _ in range(self.cols)]

    def restart(self):
        for x in range(1, self.cols - 1):
            for y in range(1, self.rows - 1):
                if dice.chance(.45):
                    self.world.set_tile((x, y), self.wall, False)
                else:
                    self.world.set_tile((x, y), self.floor, True)

    def apply_rule(self):
        for x in range(1, self.cols - 1):
            for y in range(1, self.rows - 1):
                # based off of 4-5 rule on roguebasin
                # alive if few close neighbors and many far neighbors
                # giving rise to twisted corriders about 4 or 5 tiles wide
                self.buffer[x][y] = (self.wallcount(x, y, 1) < 5 and
                                     self.wallcount(x, y, 2) > 3)

        for x in range(self.cols):
            for y in range(self.rows):
                if self.buffer[x][y]:
                    self.world.set_tile((x, y), self.floor, True)
                else:
                    self.world.set_tile((x, y), self.wall, False)

    def wallcount(self, x, y, r):
        count = 0
        for dx in range(max(0, x - r), min(self.cols, x + r + 1)):
            for dy in range(max(0, y - r), min(self.rows, y + r + 1)):
                if not self.world.passable_at((dx, dy)):
                    count += 1
        return count

    def try_connect(self):
        fill = set()
        stack = [self.world.get_passable_tile()]
        while stack:
            pos = stack.pop()
            if pos in fill or not self.world.passable_at(pos):
                continue
            fill.add(pos)
            stack.append(pos + (1, 0))
            stack.append(pos + (-1, 0))
            stack.append(pos + (0, 1))
            stack.append(pos + (0, -1))
            stack.append(pos + (1, 1))
            stack.append(pos + (1, -1))
            stack.append(pos + (-1, 1))
            stack.append(pos + (-1, -1))

        # this is rare on big maps, and cheap on small maps
        if len(fill) / (self.cols * self.rows) < .45:
            return False

        for x in range(self.cols):
            for y in range(self.rows):
                pos = x, y
                if pos not in fill and self.world.passable_at(pos):
                    self.world.set_tile(pos, self.wall, False)

        return True
